﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Schedulers;

namespace APNS.NET
{

    // The main service that is responsible for sending notifications.
    // This will create a task that will process the notification. Each task will 
    // be executed concurrently occording to the minimum concurrency level. 
    //
    // Each task will aquire an APN connection from the connection pool, send the notification using
    // the connection and then release the connection back to the pool once it is complete.
    // This allows each task to reuse a connection, avoiding creating and closing many connections.
    public class ApnsNotificationChannel : IDisposable
    {

        private readonly TaskScheduler _notificationTaskScheduler;
        private readonly ApnsConnectionPool _connectionPool;
        private readonly List<Task> _runningTasks = new List<Task>();
        private readonly List<NotificationWithHistory> _notifications = new List<NotificationWithHistory>();
        private readonly int _maxSendAttempts;

        private const int DefaultRetries = 3;
        private const int DefaultDegreeOfParallelism = 3;

        private const string DefaultSandboxHost = "gateway.sandbox.push.apple.com";
        private const string DefaultProductionHost = "gateway.push.apple.com";
        private const int DefaultPort = 2195;

        public static ApnsNotificationChannel OpenNotificationChannel(X509Certificate apnCertificate, bool useSandbox)
        {
            if (useSandbox)
                return OpenNotificationChannel(DefaultSandboxHost, DefaultPort, apnCertificate);
            else
                return OpenNotificationChannel(DefaultProductionHost, DefaultPort, apnCertificate);
        }

        public static ApnsNotificationChannel OpenNotificationChannel(string hostname, int port, X509Certificate apnCertificate)
        {
            if (apnCertificate == null) 
                throw new ArgumentNullException("apnCertificate");

            return new ApnsNotificationChannel(new ApnsConnectionPool(hostname, port, apnCertificate), DefaultDegreeOfParallelism, DefaultRetries);
        }

        internal ApnsNotificationChannel(ApnsConnectionPool connectionPool, int maxDegreeOfParallelism, int maxSendAttempts)
        {
            this._notificationTaskScheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism);
            this._connectionPool = connectionPool;
            this._maxSendAttempts = maxSendAttempts;
        }

        ~ApnsNotificationChannel()
        {
            Dispose(false);
        }
            
        public void SendNotification(ApnsNotification notification)
        {
            if(notification==null)
                throw new ArgumentNullException("notification");

            // Make sure we wrap our notification in a NotificationWithHistory class so that we can track
            // the status of it
            NotificationWithHistory notificationWithHistory = notification as NotificationWithHistory;
            if(notificationWithHistory==null)
                notificationWithHistory = new NotificationWithHistory(notification);
            _notifications.Add(notificationWithHistory);

            // Send the notification
            var task = SendNotification(notificationWithHistory);

            // Keep track of the tasks we start
            _runningTasks.Add(task);
            _runningTasks.RemoveAll(t => t.IsCompleted || t.IsFaulted);
        }
        
        private Task SendNotification(NotificationWithHistory notification)
        {
            Task result = null;
            if (notification.SendAttempts < _maxSendAttempts)
            {
                result = Task.Factory.StartNew(
                                () => ProcessNotification(notification),
                                CancellationToken.None,
                                TaskCreationOptions.AttachedToParent,
                                _notificationTaskScheduler);
            }
            else
            {
                notification.Abandoned = true;
            }
            return result;
        }

        private void ProcessNotification(NotificationWithHistory notification)
        {
            bool hasError = false;
            IApnsConnection connection = _connectionPool.GetConnection();
            try
            {
                notification.SendAttempts++;
                var response = connection.TransmitNotification(notification);
                
                if (response != null && response.StatusCode!=NotificationStatusCode.NoErrorsEncountered)
                {                   
                    hasError = true;
                    int notificationIndex = response.NotificationId;

                    NotificationWithHistory failedNotification;
                    SendResult sendResult;

                    failedNotification = (NotificationWithHistory)connection.GetTransmittedNotificationWithId(notificationIndex);
                    sendResult = new SendResult
                                     {
                                         Success = false,
                                         FailReason = ApnsFailureReason.ProcessingError,
                                         StatusCode = response.StatusCode
                                     };
                    failedNotification.ResultHistory.Add(sendResult);

                    if (response.StatusCode == NotificationStatusCode.NoneUnknown ||
                        response.StatusCode == NotificationStatusCode.ProcessingError)
                    {
                        // If there is no indication that there is an error with the notification content
                        // then we should retry the message. 
                        // Otherwise we will skip the failed notification and resend messages that were sent 
                        // Afterwards on a different connection
                        SendNotification(failedNotification);
                    }
                    else
                    {
                        failedNotification.Abandoned = true;
                    }

                    foreach (var transmittedNotification in connection.GetTransmittedNotifications(notificationIndex))
                    {
                        failedNotification = (NotificationWithHistory)transmittedNotification;
                        sendResult = new SendResult
                                         {
                                             Success = false,
                                             FailReason = ApnsFailureReason.PreviousNotificationFailed,
                                             StatusCode = NotificationStatusCode.NoneUnknown
                                         };
                        failedNotification.ResultHistory.Add(sendResult);
                        SendNotification(failedNotification);
                    }
                }
                else if (!connection.Connected)
                {
                    hasError = true;

                    // If the connection is no longer connected but we didn't get an error back assume that only
                    // the last notification attempt failed and try to resend                    
                    var sendResult = new SendResult
                                         {
                                             Success = false,
                                             FailReason = ApnsFailureReason.ConnectionDropped,
                                             StatusCode = NotificationStatusCode.NoneUnknown
                                         };
                    notification.ResultHistory.Add(sendResult);
                    SendNotification(notification);
                }
                
            }
            finally
            {
                _connectionPool.ReleaseConnection(connection, hasError);
            }
        }

        public IDictionary<ApnsNotification, IEnumerable<SendResult>> Close()
        {
            Task.WaitAll(_runningTasks.ToArray());

            // We know that we are not going to get any more results so mark those that
            // haven't been abandoned as success
            foreach (var notification in 
                _notifications.Where(n=>n.SendAttempts>0 && n.Abandoned==false))
            {
                var sendResult = new SendResult();
                sendResult.Success = true;
                sendResult.StatusCode = NotificationStatusCode.NoErrorsEncountered;
                sendResult.FailReason = ApnsFailureReason.None;

                notification.ResultHistory.Add(sendResult);
            }

            return _notifications.Where(n => n.SendAttempts > 0).ToDictionary(
                        n => n.InternalNotification,
                        n => (IEnumerable<SendResult>)n.ResultHistory);
        }

        public void Dispose()
        {
            GC.SuppressFinalize(this);
            Dispose(true);
        }
        
        public void Dispose(bool disposing)
        {
            Close();
        }

    }
}
